package net.jmchilton.spring.collect;

import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.BEAN_NAME_DELIMITERS;
import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.DEFAULT_VALUE;
import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.DEPENDS_ON_ATTRIBUTE;
import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.DESCRIPTION_ELEMENT;
import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.LAZY_INIT_ATTRIBUTE;
import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.NAME_ATTRIBUTE;
import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.PRIMARY_ATTRIBUTE;
import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.SCOPE_ATTRIBUTE;
import static org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.TRUE_VALUE;

import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

/**
 * 
 * @author John Chilton
 *
 */
abstract class TemplateBeanParser extends AbstractBeanDefinitionParser {
  private final String className;
  private final String factoryMethodName;
  
  TemplateBeanParser(final String className, final String factoryMethodName) {
    this.className = className;
    this.factoryMethodName = factoryMethodName;
  }
  
  protected abstract void parseChildren(Element element, ParserContext parserContext, AbstractBeanDefinition definition);
  
  private static String getId(final Element element, final ParserContext parserContext, final AbstractBeanDefinition definition) {
    String id = element.getAttribute(ID_ATTRIBUTE);
    if (!StringUtils.hasText(id)) {
      id = parserContext.getReaderContext().generateBeanName(definition);
    }
    return id;
  }

  private static String[] getAliases(final Element element, final ParserContext parserContext) {
    final String nameAttr = element.getAttribute(NAME_ATTRIBUTE);
    String[] aliases = null;
    if (StringUtils.hasLength(nameAttr)) {
      aliases = StringUtils.tokenizeToStringArray(nameAttr, BEAN_NAME_DELIMITERS);
    } 
    return aliases;
  }

  private AbstractBeanDefinition getNewAbstractBeanDefinition() {
    try {
      return BeanDefinitionReaderUtils.createBeanDefinition(null, this.className, null);
    } catch (ClassNotFoundException e) {
      throw new IllegalStateException(e);
    }    
  }

  private static void handleScope(final AbstractBeanDefinition definition, final Element element) {
    if (element.hasAttribute(SCOPE_ATTRIBUTE)) {
      definition.setScope(element.getAttribute(SCOPE_ATTRIBUTE));
    }
  }
  
  private static void handleDescription(final AbstractBeanDefinition definition, final Element element) {
    final Element descriptionElement = DomUtils.getChildElementByTagName(element, DESCRIPTION_ELEMENT);
    if(descriptionElement != null) {
      definition.setDescription(DomUtils.getTextValue(descriptionElement));
      element.removeChild(descriptionElement);
    }    
  }
  
  private static void handleLazyInit(final AbstractBeanDefinition definition, final Element element, final BeanDefinitionParserDelegate delegate) {    
    String lazyInit = element.getAttribute(LAZY_INIT_ATTRIBUTE);
    if (DEFAULT_VALUE.equals(lazyInit) && definition.isSingleton()) {
      // Just apply default to singletons, as lazy-init has no meaning for prototypes.
      lazyInit = delegate.getDefaults().getLazyInit();
    }
    definition.setLazyInit(TRUE_VALUE.equals(lazyInit));    
  }

  /* This doesn't seem relevant.
  private static void handleDependencyCheck(final AbstractBeanDefinition definition, final Element element, final BeanDefinitionParserDelegate delegate) {
    // Handle dependency-check
    String dependencyCheck = element.getAttribute(DEPENDENCY_CHECK_ATTRIBUTE);
    definition.setDependencyCheck(delegate.getDependencyCheck(dependencyCheck));    
  }
  */
  
  private static void handleDependsOn(final AbstractBeanDefinition definition, final Element element) {
    if (element.hasAttribute(DEPENDS_ON_ATTRIBUTE)) {
      String dependsOn = element.getAttribute(DEPENDS_ON_ATTRIBUTE);
      definition.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, BEAN_NAME_DELIMITERS));
    }    
  }

  private static void handlePrimary(final AbstractBeanDefinition definition, final Element element) {
    if (element.hasAttribute(PRIMARY_ATTRIBUTE)) {
      definition.setPrimary(TRUE_VALUE.equals(element.getAttribute(PRIMARY_ATTRIBUTE)));
    }
  }
  
  private static void registerDefinition(final AbstractBeanDefinition definition, final Element element, final ParserContext parserContext) {    
    final String id = getId(element, parserContext, definition);
    final String[] aliases = getAliases(element, parserContext);
    final BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
    BeanDefinitionReaderUtils.registerBeanDefinition(holder,  parserContext.getRegistry());
  }

  @Override
  protected AbstractBeanDefinition parseInternal(final Element element, final ParserContext parserContext) {    
    final BeanDefinitionParserDelegate delegate = parserContext.getDelegate();
    final AbstractBeanDefinition definition = getNewAbstractBeanDefinition();
    handleScope(definition, element);
    handleDescription(definition, element);
    handleLazyInit(definition, element, delegate);
    handleDependsOn(definition, element);
    handlePrimary(definition, element); 
    
    definition.setBeanClassName(this.className);
    definition.setFactoryMethodName(this.factoryMethodName);
    parseChildren(element, parserContext, definition);    
    registerDefinition(definition, element, parserContext);
    return definition;
  }

}